package helpers

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"hash/crc32"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"repair/logger"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"
)

var (
	GenerateHashesFlag bool
	CheckHashesFlag    bool
	CompareHashesFlag  bool
	CompareHashesFile1 string
	CompareHashesFile2 string
	Algorithms         []string
	HashesOutputFile   string
)

type fileInfo struct {
	Path             string            `json:"Path"`
	Size             int64             `json:"Size,omitempty"`
	SymlinkTo        string            `json:"SymlinkTo,omitempty"`
	Mode             os.FileMode       `json:"Mode,omitempty"`
	ModificationTime *time.Time        `json:"ModificationTime,omitempty"`
	Hashes           map[string]string `json:"Hashes,omitempty"`
}
type hashesDiffMap map[string][]string
type manifestDiffStruct struct {
	MissingLocalFiles   []string
	RedundantLocalFiles []string
	HashesDiff          hashesDiffMap
}
type manifestInstallationInfo struct {
	Name        string
	Version     string
	BuildNumber string
	ProductCode string
}
type ManifestStruct struct {
	InstallationInfo manifestInstallationInfo
	FilesInfo        []fileInfo
}

var installationFilesWitHashes []fileInfo
var manifest ManifestStruct

func GetManifestStruct() ManifestStruct {
	if len(manifest.FilesInfo) > 0 {
		return manifest
	} else {
		return GenerateMainfest()
	}
}
func GetInstallationFilesWithHashes() []fileInfo {
	if len(installationFilesWitHashes) > 0 {
		return installationFilesWitHashes
	} else {
		return GenerateFilesInfo()
	}
}
func GenerateMainfest() (manifestAsStruct ManifestStruct) {
	manifestAsStruct.FilesInfo = GetInstallationFilesWithHashes()
	installationInfo := CurrentIde.GetInfo()
	manifestAsStruct.InstallationInfo.Name = installationInfo.Name
	manifestAsStruct.InstallationInfo.Version = installationInfo.Version
	manifestAsStruct.InstallationInfo.BuildNumber = installationInfo.BuildNumber
	manifestAsStruct.InstallationInfo.ProductCode = installationInfo.ProductCode
	return manifestAsStruct
}
func GenerateMainfestFile() {
	logger.ConsoleLogger.Println("hashes are being generated...")
	filesInfoAsBytes := convertAnyToJson(GetManifestStruct())
	if FileExists(HashesOutputFile) {
		_ = os.Remove(HashesOutputFile)
	}
	WriteToFile(string(filesInfoAsBytes), HashesOutputFile)
}
func CompareHashes() {
	file1, _ := ioutil.ReadFile(CompareHashesFile1)
	file2, _ := ioutil.ReadFile(CompareHashesFile2)
	manifestStruct1 := convertJsonToSlice(file1)
	manifestStruct2 := convertJsonToSlice(file2)
	if manifestDiff, err := compareManifests(manifestStruct1, manifestStruct2); err == nil {
		if len(manifestDiff.HashesDiff) > 0 || len(manifestDiff.MissingLocalFiles) > 0 || len(manifestDiff.RedundantLocalFiles) > 0 {
			if len(manifestDiff.HashesDiff) > 0 {
				logger.DebugLogger.Println(string(convertAnyToJson(manifestDiff)))
				logger.ErrorLogger.Println("Hashes for the following files do not match:" + sliceAsString(manifestDiff.HashesDiff.getFilesSlice()))
			}
			if len(manifestDiff.MissingLocalFiles) > 0 {
				logger.ErrorLogger.Println(CompareHashesFile2 + " missing the following: " + sliceAsString(manifestDiff.MissingLocalFiles))
			}
			if len(manifestDiff.RedundantLocalFiles) > 0 {
				logger.ErrorLogger.Println(CompareHashesFile1 + " missing the following: " + sliceAsString(manifestDiff.RedundantLocalFiles))
			}
		} else {
			logger.InfoLogger.Println("No illegal files found")
		}

	} else {
		logger.ErrorLogger.Println(err)
	}

}
func CheckHashes() {
	logger.InfoLogger.Println("Downloading manifest file")
	manifestJson, err := downloadManifest(CurrentIde)
	if err != nil {
		logger.WarningLogger.Printf("Unable to download manifest file from %s: \n\t %s", generateManifestUrl(CurrentIde), err.Error())
		return
	} else {
		logger.DebugLogger.Printf("Downloaded manifest from %s", generateManifestUrl(CurrentIde))
	}
	manifestStruct := convertJsonToSlice(manifestJson)
	logger.InfoLogger.Println("Hashes are being checked")
	if manifestDiff, err := compareManifests(manifestStruct, GetManifestStruct()); err == nil {
		if len(manifestDiff.HashesDiff) > 0 {
			logger.DebugLogger.Println(string(convertAnyToJson(manifestDiff)))
			logger.ErrorLogger.Println("Hashes for the following files does not match the hashes generated by JetBrains:" + sliceAsString(manifestDiff.HashesDiff.getFilesSlice()))
			AskUserToDownloadFreshInstallation()
		} else if len(manifestDiff.MissingLocalFiles) > 0 {
			logger.ErrorLogger.Println("The following files were not found in the installation:" + sliceAsString(manifestDiff.MissingLocalFiles))
		} else if len(manifestDiff.RedundantLocalFiles) > 0 {
			logger.WarningLogger.Println("Installation directory contains files which aren’t part of distribution:" + sliceAsString(manifestDiff.RedundantLocalFiles))
			AskUserAndRemoveFiles(manifestDiff.RedundantLocalFiles)
		} else {
			logger.InfoLogger.Println("No illegal files found")
		}
	} else {
		logger.ErrorLogger.Println(err)
	}
}

func (m *hashesDiffMap) getFilesSlice() (slice []string) {
	for i := range *m {
		slice = append(slice, i)
	}
	return slice
}
func sliceAsString(slice []string) (sliceAsList string) {
	for _, sliceElement := range slice {
		sliceAsList = sliceAsList + "\n\t" + sliceElement
	}
	return sliceAsList
}

func compareManifests(remoteManifestStruct ManifestStruct, localManifestStruct ManifestStruct) (manifestDiffStruct, error) {
	if !CheckIfManifestBalongsToInstallation(remoteManifestStruct.InstallationInfo, localManifestStruct.InstallationInfo) {
		return manifestDiffStruct{}, errors.New("Manifest file does not belong to the local installation.")
	}
	var diff manifestDiffStruct
	diff.HashesDiff = make(map[string][]string)
	for i := 0; i < 2; i++ {
		for _, remoteFile := range remoteManifestStruct.FilesInfo {
			found := false
			for _, localFile := range localManifestStruct.FilesInfo {
				if remoteFile.Path == localFile.Path {
					found = true
					for alg := range remoteFile.Hashes {
						if remoteFile.Hashes[alg] != localFile.Hashes[alg] && !(len(diff.HashesDiff[remoteFile.Path]) != 0) {
							diff.HashesDiff[remoteFile.Path] = []string{remoteFile.Hashes[alg], localFile.Hashes[alg]}
						}
					}
					break
				}
			}
			if !found && !sliceOfStringsContains(ManifestFilesExclusions[runtime.GOOS], remoteFile.Path) {
				diff.RedundantLocalFiles = append(diff.RedundantLocalFiles, remoteFile.Path)
			}
		}
		if i == 0 {
			remoteManifestStruct, localManifestStruct = localManifestStruct, remoteManifestStruct
			diff.MissingLocalFiles, diff.RedundantLocalFiles = diff.RedundantLocalFiles, diff.MissingLocalFiles
		}
	}

	return diff, nil
}

func CheckIfManifestBalongsToInstallation(remoteInfo manifestInstallationInfo, localInfo manifestInstallationInfo) bool {
	if remoteInfo.BuildNumber == localInfo.BuildNumber {
		return true
	}
	logger.InfoLogger.Println(remoteInfo.BuildNumber + "!=" + localInfo.BuildNumber)
	return false
}

func convertJsonToSlice(manifestJson []byte) (manifest ManifestStruct) {
	err := json.Unmarshal(manifestJson, &manifest)
	if err == nil {
		return manifest
	}
	logger.WarningLogger.Println("Error reading manifest file: " + err.Error())
	return ManifestStruct{}
}

func downloadManifest(ide IDE) (json []byte, err error) {
	if resp, err := http.Get(generateManifestUrl(ide)); err == nil {
		defer resp.Body.Close()
		if resp.StatusCode != 200 {
			return nil, errors.New("Error downloading Manifest file. Response status = " + strconv.Itoa(resp.StatusCode))
		}
		if json, err = ioutil.ReadAll(resp.Body); err == nil {
			return json, err
		}
	}
	return nil, err

}

func generateManifestUrl(ide IDE) string {
	url := ide.GetManifestDownloadUrl()
	return url
}

func convertAnyToJson(toConvert interface{}) (result []byte) {
	marshal, _ := json.Marshal(toConvert)
	return marshal
}

var manifestGenerationWorkGroup sync.WaitGroup

type ProtectedFilesStruct struct {
	mu         sync.Mutex
	distrFiles []fileInfo
}

func GenerateFilesInfo() (distrFiles []fileInfo) {
	ideaBinary := GetIdeaBinaryToWrokWith()
	ideaPath := GetIdeIdePackageByBinary(ideaBinary)
	timebeginning := time.Now().Unix()
	var distrFilesThreaded ProtectedFilesStruct
	manifestGenerationWorkGroup.Add(1)
	go walkAndCollectHashes(ideaPath, &distrFilesThreaded)
	manifestGenerationWorkGroup.Wait()
	distrFiles = distrFilesThreaded.distrFiles
	sort.Slice(distrFiles[:], func(i, j int) bool {
		return strings.Compare(distrFiles[i].Path, distrFiles[j].Path) == -1
	})
	logger.InfoLogger.Printf("File struct size: %d, Time %d sec", len(distrFiles), time.Now().Unix()-timebeginning)
	return distrFiles
}

func walkAndCollectHashes(currentPath string, distrFiles *ProtectedFilesStruct) {
	defer manifestGenerationWorkGroup.Done()
	visit := func(path string, file os.DirEntry, err error) error {
		if file.IsDir() && path != currentPath {
			manifestGenerationWorkGroup.Add(1)
			go walkAndCollectHashes(path, distrFiles)
			return filepath.SkipDir
		}
		var distrFile fileInfo
		distrFile.Hashes = make(map[string]string)
		distrFile.Path = filepath.ToSlash(strings.TrimPrefix(path, GetIdePackageToWorkWith(GetIdeaBinaryToWrokWith())+string(os.PathSeparator)))
		if file.Type().IsRegular() {
			fillFileInfoStruct(path, file, &distrFile)
		} else if file.Type()&os.ModeSymlink != 0 {
			link, _ := os.Readlink(path)
			distrFile.SymlinkTo = link
		} else if file.IsDir() {
			return nil
		}
		distrFiles.mu.Lock()
		distrFiles.distrFiles = append(distrFiles.distrFiles, distrFile)
		distrFiles.mu.Unlock()
		return nil
	}
	_ = filepath.WalkDir(currentPath, visit)
}

func fillFileInfoStruct(path string, file os.DirEntry, distrFile *fileInfo) {
	info, _ := file.Info()
	distrFile.Size = info.Size()
	distrFile.Mode = info.Mode()
	date := info.ModTime().Local()
	distrFile.ModificationTime = &date
	for _, algorithm := range Algorithms {
		distrFile.Hashes[algorithm] = getHash(path, algorithm)
	}
}

func getHash(path string, algorithm string) string {
	f, err := os.Open(path)
	logger.ExitWithExceptionOnError(err)
	defer func() {
		_ = f.Close()
	}()

	copyBuf := make([]byte, 1024*1024)

	if algorithm == "CRC32" {
		tablePolynomial := crc32.MakeTable(0xedb88320)
		h := crc32.New(tablePolynomial)
		if _, err := io.CopyBuffer(h, f, copyBuf); err != nil {
			logger.ExitWithExceptionOnError(err)
		}

		return hex.EncodeToString(h.Sum(nil))
	} else if algorithm == "SHA256" {
		h := sha256.New()
		if _, err := io.CopyBuffer(h, f, copyBuf); err != nil {
			logger.ExitWithExceptionOnError(err)
		}
		return hex.EncodeToString(h.Sum(nil))
	} else {
		logger.ExitWithExceptionOnError(errors.New(algorithm + " is not yet supported"))
		return ""
	}
}
